# 普通套利，参与市场有Okcoin，火币，btctrade，BTCC，中国比特币
# 但事实证明，这种套利效果并不明显，因为除了Ok，火币外，其他市场都是长时间单边低价，资金利用率非常低，所以舍弃其他市场
# 总之，此策略更适合Okcoin与火币

import markets
import config
import time
from concurrent.futures import ThreadPoolExecutor
from lib.mysqlhelper import MysqlHelper
from operator import attrgetter

exchanges = []
total_cny = 0.0
total_btc = 0.0
init_btc = 0.0 #操作开始前，比特币数量
init_cny = 0.0 #操作开始前，人民币数量


def init_exchanges():
    exchange_names = config.exchanges
    global exchanges #哪里需要使用全局变量，哪里就声明一下
    for exchange_name in exchange_names:
        try:
            exec('import markets.' + exchange_name.lower())
            exchange = eval('markets.' + exchange_name.lower() + '.' + exchange_name + '()')
            exchanges.append(exchange)
            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
            print("%s: Init exchange %s." % (datetime, exchange_name))
        except Exception as e:
            print("%s exchange name is invalid, please check config.py" % exchange_name)
            print(e)
    print("########################################")


# def init_params():
#     global total_btc, total_cny, init_btc, init_cny
#     total_cny = 0.0
#     total_btc = 0.0
#     init_btc = 0.0  # 操作开始前，比特币数量
#     init_cny = 0.0  # 操作开始前，人民币数量
#     datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
#     print("%s: Init global params!" % datetime)


def checkProfitIn24h(): #检查24小时内的收益情况
    pass


# decimal表示浮点数值，number表示保留小数位数；number只能为1,2,3,4
def retainDigiNum(decimal, number): # 保留N位小数，不四舍五入；最好别用round函数，有精度误差的问题，会产生特别长的浮点数
    if number == 1:
        return float(("%.2f" % decimal)[0:-1])
    if number == 2:
        return float(("%.3f" % decimal)[0:-1])
    if number == 3:
        return float(("%.4f" % decimal)[0:-1])
    if number == 4:
        return float(("%.5f" % decimal)[0:-1])


def cancelAll():
    global exchanges
    for exchange in exchanges:
        exchange.cancel_all()
    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
    print("%s: Cancel all orders!" % datetime)


def updateCurrentDepth():
    global exchanges
    threadpool = ThreadPoolExecutor(max_workers=10)
    for exchange in exchanges:
        threadpool.submit(exchange.update_depth)
    threadpool.shutdown(wait=True)


def updateCurrentAsset():
    global exchanges
    threadpool = ThreadPoolExecutor(max_workers=10)
    for exchange in exchanges:
        threadpool.submit(exchange.update_accountInfo)
    threadpool.shutdown(wait=True)

    global total_cny, total_btc
    total_btc = 0.0
    total_cny = 0.0
    for exchange in exchanges:
        total_cny += exchange.cny_free
        total_cny += exchange.cny_frozen
        total_btc += exchange.btc_free
        total_btc += exchange.btc_frozen


def getProfit():
    global total_cny, init_cny
    updateCurrentAsset()
    return total_cny - init_cny


def balanceBtc(): # 平衡一轮操作前后的BTC数量，保持BTC量前后一致
    global total_btc, init_btc, exchanges
    cancelAll()
    updateCurrentAsset()
    diff = retainDigiNum(total_btc - init_btc, 3)
    updateCurrentDepth()
    if -config.MaxAmountOnce <= diff <= -config.MinAmountOnce: # 假如不平衡量小于最小交易值（大于最大交易值），不处理
        balance_exchanges = sorted(exchanges, key=attrgetter('ask'))
        for exchange in balance_exchanges:
            if exchange.cny_free < abs(diff) * (exchange.ask + config.SlidePrice):
                continue
            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))) # 故意放在这里，避免跟利润统计的时间一样
            exchange.buy(exchange.ask + config.SlidePrice, abs(diff))
            time.sleep(3)
            record = MysqlHelper()
            record.record_op(datetime, "enbalanceBTC", exchange.__class__.__name__, None,
                                 exchange.ask + config.SlidePrice, abs(diff), None)
            record.end()
            break

    if config.MinAmountOnce <= diff <= config.MaxAmountOnce:
        balance_exchanges = sorted(exchanges, key=attrgetter('bid'), reverse=True)
        for exchange in balance_exchanges:
            if exchange.btc_free < diff:
                continue
            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
            exchange.sell(exchange.bid - config.SlidePrice, diff)
            time.sleep(3)
            record = MysqlHelper()
            record.record_op(datetime, "enbalanceBTC", None, exchange.__class__.__name__,
                                 exchange.bid - config.SlidePrice, diff, None)
            record.end()
            break


def onTick():
    global exchanges, init_cny, init_btc, total_btc, total_cny

    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
    updateCurrentAsset()
    init_cny = total_cny
    init_btc = total_btc
    print("%s: Asset is updated! init_cny: %f, init_btc: %f." % (datetime, init_cny, init_btc))

    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
    updateCurrentDepth()
    print("%s: Depth is updated!" % datetime)
    for exchange in exchanges:
        print("%8s, bid: %7.2f, ask: %7.2f;  btc_free: %10.6f,  cny_free: %9.4f" %
              (exchange.__class__.__name__, exchange.bid, exchange.ask, exchange.btc_free, exchange.cny_free))

    buy_exchanges = sorted(exchanges, key=attrgetter('ask'), reverse=False)
    sell_exchanges = sorted(exchanges, key=attrgetter('bid'), reverse=True)

    flag = False
    for buy_exchange in buy_exchanges:
        for sell_exchange in sell_exchanges:
            if buy_exchange == sell_exchange:  # 排除相同的交易所
                continue
            if buy_exchange.bid == 0 or sell_exchange.bid == 0:  # 排除交易所深度更新不成功；只要更新不成功，四个参数都为0，取其中任一个判断
                continue
            if sell_exchange.bid * (1 - sell_exchange.fee / 100) - buy_exchange.ask * \
                    (1 + buy_exchange.fee / 100) < config.MinDiff: # 排除价差不够
                continue  # 思考：能不能简化步骤，将分析和买卖合并
            maxVolume = max(config.MinAmountOnce, min(retainDigiNum(buy_exchange.ask_volume, 3), retainDigiNum(sell_exchange.bid_volume, 3))) # 最大可交易量
            price = retainDigiNum((sell_exchange.bid + buy_exchange.ask)/2, 2)

            if (buy_exchange.cny_free < config.MinAmountOnce * price) or (sell_exchange.btc_free < config.MinAmountOnce): # 排除可用资金不够
                continue

            volume = min(config.MaxAmountOnce, retainDigiNum(buy_exchange.cny_free/price, 3), retainDigiNum(sell_exchange.btc_free, 3), maxVolume) # 实际下单币数取交易所可交易数，最大可交易数，最大交易数限制，四者最小值

            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))) # 记录买卖操作的时间
            threadpool = ThreadPoolExecutor(max_workers=2)
            buy_future = threadpool.submit(buy_exchange.buy, price, volume)
            sell_future = threadpool.submit(sell_exchange.sell, price, volume)
            try:
                buy_result = buy_future.result(timeout=10)
                sell_result = sell_future.result(timeout=10)
            except Exception as e:
                buy_result = False
                sell_result = False
                print(e)

            record = MysqlHelper()
            record.record_op(datetime, "trade", buy_exchange.__class__.__name__, sell_exchange.__class__.__name__, price, volume, None)

            time.sleep(5)
            buy_order_status = buy_exchange.order_info(buy_result)
            sell_order_status = sell_exchange.order_info(sell_result)

            if buy_order_status == (False or 'cancelable'):
                datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                record.record_op(datetime, "error", None, None, None, None,
                                 buy_exchange.__class__.__name__ + " dot not completely deal")
            if sell_order_status == (False or 'cancelable'):
                datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                record.record_op(datetime, "error", None, None, None, None,
                                 sell_exchange.__class__.__name__ + " dot not completely deal")

            if (buy_order_status == (False or 'cancelable')) or (sell_order_status == (False or 'cancelable')):
                balanceBtc()

            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
            profit = retainDigiNum(getProfit(), 4) # 此操作会更新资产信息
            if abs(profit) > config.MaxDiff * volume: # 按常理来说，每次套利的利润应该是很低的，人民币总额波动不会太大，大于理论值就记为零
                profit = 0
            print("%s: Current total_cny: %f, total_btc: %f" % (datetime, total_cny, total_btc))
            record.record_op(datetime, "profit", None, None, None, profit, "total_cny: %f, total_btc: %f" % (total_cny, total_btc))
            record.end()
            flag = True

            break
        if flag:
            break
    if not flag:
        datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
        print("%s: No opportunity to arbitrage." % datetime)
    print("----------------------------------------")


def main():
    init_exchanges()
    while True:
        cancelAll()
        onTick()
        time.sleep(3)


if __name__ == '__main__':
    main()
